#!/usr/bin/env python
# -*- coding: utf-8 -*-

# This application is released under the GNU General Public License 
# v2 (or, at your option, any later version). You can find the full 
# text of the license under http://www.gnu.org/licenses/gpl.txt. 
# By using, editing and/or distributing this software you agree to 
# the terms and conditions of this license. 
# Thank you for using free software!

#  TwitterScreenlet (c) papamitra 2008

import screenlets
# from screenlets import utils
from screenlets.options import StringOption , BoolOption , IntOption , FileOption , DirectoryOption , ListOption , AccountOption , TimeOption , FontOption, ColorOption , ImageOption
from screenlets.options import create_option_from_node
from screenlets import DefaultMenuItem

import pango
import cairo
import sys
import gtk
import gobject
import os
import re
import twitter
import urllib
import xml.sax.saxutils

from ConfigParser import SafeConfigParser

from ThemeParser import ThemeParser

import traceback

PI = 3.141596

class UserInfo:
    def __init__ (self, name=None, screen_name=None, iconPixbuf=None):
        self.name = name
        self.screen_name = screen_name
        self.iconPixbuf = iconPixbuf

# get from PidginScreenelt
def scaleGdkPixbuf(pixbuf,size):

    newpixbuf = None
    try:
        icw = pixbuf.get_width()
        ich = pixbuf.get_height()
        aspect = float(icw)/ich
        if aspect < 1: newpixbuf = pixbuf.scale_simple(int(aspect*size),size,1)
        elif aspect > 1: newpixbuf = pixbuf.scale_simple(size,int(size/aspect),1)
        else: newpixbuf = pixbuf.scale_simple(size,size,1)
    except:
        None
    return newpixbuf

def escape_for_pango_markup(string):
    # escapes < > & ' "
    # for pango markup not to break
    if string is None:
        return
    if gtk.pygtk_version >= (2, 8, 0) and gtk.gtk_version >= (2, 8, 0):
        escaped_str = gobject.markup_escape_text(string)
    else:
        escaped_str = xml.sax.saxutils.escape(string, {"'": '&apos;',
                                                       '"': '&quot;'})

    return escaped_str


class TwitterScreenlet (screenlets.Screenlet):
    """A screenlet to show Twitter timeline"""

    # default meta-info for Screenlets (should be removed and put into metainfo)
    __name__	= 'TwitterScreenlet'
    __version__	= '0.1.1'
    __author__	= 'papamitra@gmail.com'
    __desc__	= __doc__	# set description to docstring of class

    __api = None # twitter API
    __update_method = None

    __timeout = None
    
    __fixed = None
    __entry = None

    __status_on_tooltip = None

    item_font = "Sans Medium 5"
    account = ('', '')
    statuses = []
    users = {}
    icon_cache = {}
    layout = None

    show_public_timeline = True
    num_of_statuses = 5
    update_interval = 5

    header_height = 30
    footer_height = 35
    user_icon_size = 30
    status_height = 48

    def __init__ (self, **keyword_args):
        screenlets.Screenlet.__init__(self, width=200, height=200, 
                                      uses_theme=True, **keyword_args)
        if not self.theme:
            self.theme_name="default"

        self.add_options_group('Twitter', 'The twitter widget settings')

        self.add_option(BoolOption('Twitter', 'show_public_timeline', self.show_public_timeline,
                                   'Show Public Timeline',''))

        self.add_option(AccountOption('Twitter', 'account', 
                                      self.account, 'Username/Password', 
                                      'Enter username/password here ...'))
        
        self.add_option(IntOption('Twitter', 'update_interval', 
                                  self.update_interval, 'Update interval', 
                                  'The interval for refreshing status (in minutes)', min=1, max=60))

        self.add_option(IntOption('Twitter', 'num_of_statuses', 
                                  self.num_of_statuses, 'Number of statuses to display', 
                                  'Number of statuses to display',  min=1, max=20))

        self.add_option(IntOption('Twitter', 'status_height',
                                  self.status_height, 'Height of status to display', 
                                  'Height of status to display',  min=1, max=99))

    def __setattr__(self, name, value):
        # call Screenlet.__setattr__ in baseclass (ESSENTIAL!!!!)
        screenlets.Screenlet.__setattr__(self, name, value)
        if name == "update_interval":
            if value > 0:
                self.__dict__['update_interval'] = value
                if self.__timeout:
                    gobject.source_remove(self.__timeout)
                self.__timeout = gobject.timeout_add(int(value * 60000), self.update)
            else:
                self.__dict__['update_interval'] = 1
                pass
        if name == 'account':
            self.__api = self.get_twitter_api(self.account[0], self.account[1])
            if self.account[0] == '':
                self.__update_method = self.__api.GetPublicTimeline
                
                self.show_public_timeline = True
            else:
                try:
                    if self.show_public_timeline:
                        self.__update_method = self.__api.GetPublicTimeline
                    else:
                        self.__update_method = self.__api.GetFriendsTimeline
                    self.statuses = {}
                    self.redraw_canvas()
                except:
                    screenlets.show_message(self, "Login Failed!")
                    self.account= ('', '')
                    self.__api = self.get_twitter_api()
                    self.__update_method = self.__api.GetPublicTimeline
                    self.show_public_timeline = True

        if name == 'show_public_timeline':
            if self.__api:
                if self.show_public_timeline == False and self.account[0] != '':
                    self.__update_method = self.__api.GetFriendsTimeline        
                else:
                    self.__update_method = self.__api.GetPublicTimeline
                
                self.statuses = {}
                self.redraw_canvas()

        if name == 'num_of_statuses':
            self.resize_and_redraw()

        if name == 'status_height':
            if value < 1:
                self.__dict__['status_height'] = 1
            
            for status in self.statuses:
                status.buffer = None
            self.resize_and_redraw()

    def resize_and_redraw(self):
        if not self.layout: return
        for status in self.statuses:
            status.buffer = None
        self.width = self.layout.window.width
        self.height = self.header_height + self.footer_height + self.statuses_height()
        self.modify_entry_pos()
        self.redraw_canvas()
        
    def border_width(self):
        if not self.layout: return 0
        ls = self.layout.status
        if ls.enable_border:
            return ls.border_width
        else:
            return 0

    def statuses_height(self):
        return self.status_height * self.num_of_statuses + self.border_width() * (self.num_of_statuses -1)

    def get_twitter_api(self, user_name = None, password= None):
        if user_name == '' :
            user_name = None
            password = None
        api = twitter.Api(user_name, password)

        # Mail to Alex...
        api.SetUserAgent('TwitterScreenlet')
        
        api.SetCacheTimeout(0)
        return api

    def modify_entry_pos(self):
        #move text entry
        if self.__entry:
            self.__entry.set_size_request(int(self.width - 20 * self.scale), -1)

        if self.__fixed:
            self.__fixed.move(self.__entry, 10, int((self.header_height + self.statuses_height() + 5) * self.scale))
        self.window.show_all()        

    def update(self):
        if self.__api and self.__update_method:
            self.get_timeline()
        self.redraw_canvas()
        return True # timeout contintue

    def get_timeline(self):
        try:
            self.statuses = self.__update_method()
        except:
            traceback.print_exc()
            screenlets.show_message(self, "Failed to get statuses")
            
        new_icon_cache = {}
        for status in self.statuses:
            status.buffer = None # buffer to display
            if not self.users.has_key(status.GetUser().GetId()):
                user_name = status.GetUser().GetName()
                screen_name = status.GetUser().GetScreenName()
                try:
                    icon_url = status.GetUser().GetProfileImageUrl().encode("ASCII")
                    if self.icon_cache.has_key(icon_url):
                        icon_pixbuf = icon_cache[icon_url]
                    else:
                        icon_path, headers = urllib.urlretrieve(icon_url)
                        icon_pixbuf = gtk.gdk.pixbuf_new_from_file(icon_path)
                        icon_pixbuf = scaleGdkPixbuf(icon_pixbuf, self.user_icon_size)

                    new_icon_cache[icon_url] = icon_pixbuf
                except:
                    print 'Failed to Get icon pixbuf.'
                    icon_pixbuf = None

                self.users[status.GetUser().GetName()] = UserInfo(user_name, screen_name, icon_pixbuf)

        icon_cache = new_icon_cache
        urllib.urlcleanup()
               
    def on_after_set_atribute(self,name, value):
        """Called after setting screenlet atributes"""
        #print name + ' is going to change from ' + str(value)
        pass

    def on_before_set_atribute(self,name, value):
        """Called before setting screenlet atributes"""
        #print name + ' has changed to ' + str(value)
        pass


    def on_create_drag_icon (self):
        """Called when the screenlet's drag-icon is created. You can supply
        your own icon and mask by returning them as a 2-tuple."""
        return (None, None)

    def on_composite_changed(self):
        """Called when composite state has changed"""
        pass

    def on_drag_begin (self, drag_context):
        """Called when the Screenlet gets dragged."""
        pass

    def on_drag_enter (self, drag_context, x, y, timestamp):
        """Called when something gets dragged into the Screenlets area."""
        pass

    def on_drag_leave (self, drag_context, timestamp):
        """Called when something gets dragged out of the Screenlets area."""
        pass

    def on_drop (self, x, y, sel_data, timestamp):
        """Called when a selection is dropped on this Screenlet."""
        return False

    def on_focus (self, event):
        """Called when the Screenlet's window receives focus."""
        pass

    def on_hide (self):
        """Called when the Screenlet gets hidden."""
        pass

    def on_init (self):
        """Called when the Screenlet's options have been applied and the 
        screenlet finished its initialization. If you want to have your
        Screenlet do things on startup you should use this handler."""
        print 'i just got started'
        # add  menu items from xml file
        self.add_default_menuitems(DefaultMenuItem.XML)
        # add menu item
        self.add_menuitem("refresh", "Refresh")
        # add default menu items
        self.add_default_menuitems()

        self.__entry = gtk.Entry(140)
        self.__entry.set_size_request(self.width,-1)
        self.__entry.set_has_frame(False)
        self.__fixed = gtk.Fixed()
        self.window.add(self.__fixed)
        self.__fixed.put(self.__entry, 0, 0)

        self.__tooltips = gtk.Tooltips()
        self.__tooltips.set_tip(self.__entry, "<Shift>Return to Post")

        self.modify_entry_pos()
        if self.__api == None:
            self.__api = self.get_twitter_api()
            self.__update_method = self.__api.GetPublicTimeline
        self.update()
        self.update_interval = self.update_interval

        self.height = self.header_height + self.footer_height + self.statuses_height()

    def on_key_down(self, keycode, keyvalue, event):
        """Called when a keypress-event occured in Screenlet's window."""

        key = gtk.gdk.keyval_name(event.keyval)

        if key == "Return" and (event.state & gtk.gdk.SHIFT_MASK):
            #screenlets.show_message(self, self.__entry.get_text())
            try:
                self.__entry.set_editable(False)
                self.__api.PostUpdate(self.__entry.get_text())
                self.__entry.set_text("")
                self.__entry.set_editable(True)
                self.update()
            except:
                pass

    def on_load_theme (self):
        """Called when the theme is reloaded (after loading, before redraw)."""
        options_file = sys.path[0]+"/themes/"+self.theme_name+"/options.conf"
        if os.path.exists(options_file): 
            self.layout = ThemeParser(options_file)
        else:
            raise "Invalid Theme. No options.conf"

        self.resize_and_redraw()
        pass

    def on_menuitem_select (self, id):
        """Called when a menuitem is selected."""
        if id == "refresh":
            print "refresh"
            cache_timeout = self.__api._cache_timeout
            try:
                self.__api.SetCacheTimeout(0)
                self.update()
            finally:
                self.__api.SetCacheTimeout(cache_timeout)

    def on_mouse_down (self, event):
        """Called when a buttonpress-event occured in Screenlet's window. 
        Returning True causes the event to be not further propagated."""

        return False

    def on_mouse_enter (self, event):
        """Called when the mouse enters the Screenlet's window."""
        #print 'mouse is over me'

    def on_mouse_leave (self, event):
        """Called when the mouse leaves the Screenlet's window."""
        self.hide_tooltip()
        self.hover = False
        self.__status_on_tooltip = None
        #print 'mouse leave'

    def on_mouse_move(self, event):
        """Called when the mouse moves in the Screenlet's window."""

        def get_status_under_pointer(x, y):
            try:
                if y > self.header_height and y < self.height - self.footer_height:
                    # FIXME:
                    return self.statuses[int((y - self.header_height)/ (self.status_height + self.border_width()) )]
                else:
                    return None
            except:
                return None

        status = get_status_under_pointer(event.x, event.y)
        if status and status != self.__status_on_tooltip:
            self.show_tooltip("%s\n%s" % (status.GetUser().GetScreenName(), 
                                          escape_for_pango_markup(status.GetText()))
                              ,self.x+self.mousex,self.y+self.mousey)
            self.hover = True
            self.__status_on_tooltip = status
        elif status == None:
            self.hide_tooltip()
            self.hover = False
            self.__status_on_tooltip = None
            
        #self.redraw_canvas()
        pass

    def on_mouse_up (self, event):
        """Called when a buttonrelease-event occured in Screenlet's window. 
        Returning True causes the event to be not further propagated."""
        return False

    def on_quit (self):
        """Callback for handling destroy-event. Perform your cleanup here!"""
        return True

    def on_realize (self):
        """"Callback for handling the realize-event."""
        pass

    def on_scale (self):
        """Called when Screenlet.scale is changed."""
        self.resize_and_redraw()
        pass

    def on_scroll_up (self):
        """Called when mousewheel is scrolled up (button4)."""
        pass

    def on_scroll_down (self):
        """Called when mousewheel is scrolled down (button5)."""
        pass

    def on_show (self):
        """Called when the Screenlet gets shown after being hidden."""
        pass

    def on_switch_widget_state (self, state):
        """Called when the Screenlet enters/leaves "Widget"-state."""
        pass

    def on_unfocus (self, event):
        """Called when the Screenlet's window loses focus."""
        pass

    def on_draw (self, ctx):
        """In here we load the theme"""

        if not self.layout: return

        l = self.layout
        ctx.scale(self.scale , self.scale)
        ctx.translate(0, 0)

        # draw header
        ctx.save()
        self.round_rectangle(ctx, 0, 0, self.width, self.header_height, l.window.radius, l.window.radius, 0, 0)
        ctx.clip()
        linear = cairo.LinearGradient(0, 0, 0, self.header_height)
        linear.add_color_stop_rgba(0, *l.header.start_color.get())
        linear.add_color_stop_rgba(1.0,*l.header.end_color.get())
        ctx.set_source(linear)
        ctx.paint()
        ctx.restore()

        if self.show_public_timeline == False and self.account[0] != '': header_str = "Friends" 
        else: header_str = "Public"
        
        ctx.save()
        ctx.set_source_rgba(*l.header.title_color.get())
        self.draw_text(ctx, header_str, 10, 5, self.item_font, 14, self.width,
                       allignment=pango.ALIGN_LEFT,
                       weight=5,
                       ellipsize=pango.ELLIPSIZE_NONE)
        ctx.restore()

        # draw twitter logo
        #ctx.save()
        #ctx.translate(120,6)
        #ctx.scale(.4,.4)
        #self.theme.render(ctx, 'logo')
        #ctx.restore()

        self.draw_status_background(ctx)

        border_width = 0
        if l.status.enable_border:
            border_width = l.status.border_width

        ctx.save()
        ctx.translate(0,self.header_height)
        for status in self.statuses:
            self.draw_user_status(ctx, status)
            ctx.translate(0, self.status_height)
            ctx.translate(0, border_width)
        ctx.restore()

        # draw footer
        ctx.save()
        ctx.translate(0, self.header_height + self.statuses_height())
        self.round_rectangle(ctx, 0, 0, self.width, self.footer_height, 0, 0, l.window.radius, l.window.radius)
        ctx.clip()
        linear = cairo.LinearGradient(0, 0, 0, self.footer_height)
        linear.add_color_stop_rgba(0.0, *l.footer.start_color.get())
        linear.add_color_stop_rgba(1.0, *l.footer.end_color.get())
        ctx.set_source(linear)
        ctx.paint()
        ctx.restore()

    def on_draw_shape (self, ctx):
        self.on_draw(ctx)


    def draw_text(self, ctx, text, x, y,  font, size, width, allignment=pango.ALIGN_LEFT,alignment=None,justify = False,weight = 0, ellipsize = pango.ELLIPSIZE_NONE, is_single_p=False):
        """Draws text"""

        ctx.save()
        ctx.translate(x, y)
        if self.p_layout == None :
            self.p_layout = ctx.create_layout()
        else:
            ctx.update_layout(self.p_layout)
        if self.p_fdesc == None:self.p_fdesc = pango.FontDescription()
        else: pass
        self.p_fdesc.set_family_static(font)
        self.p_fdesc.set_size(size * pango.SCALE)
        self.p_fdesc.set_weight(weight)
        self.p_layout.set_font_description(self.p_fdesc)
        self.p_layout.set_width(width * pango.SCALE)
        self.p_layout.set_alignment(allignment)
        if alignment != None:self.p_layout.set_alignment(alignment)
        self.p_layout.set_justify(justify)
        self.p_layout.set_ellipsize(ellipsize)
        self.p_layout.set_markup(text)
        ###
        self.p_layout.set_single_paragraph_mode(False)
        self.p_layout.set_wrap(pango.WRAP_WORD_CHAR)
        ###

        ctx.show_layout(self.p_layout)
        ctx.restore()

    def draw_status_background(self, ctx):
        """ draw status background"""
        
        if not self.layout: return

        ls = self.layout.status
        ctx.save()
        ctx.translate(0,self.header_height)
        for index in range(self.num_of_statuses):
            if  index % 2: # index 0 = line 1
                ctx.set_source_rgba(*ls.even_line_color.get())
            else:
                ctx.set_source_rgba(*ls.odd_line_color.get())
                
            self.draw_rectangle(ctx,0,0,self.width,self.status_height)           

            ctx.translate(0, self.status_height)
            if ls.enable_border and index < self.num_of_statuses - 1:
                ctx.save()
                ctx.set_source_rgba(*ls.border_color.get())
                self.draw_rectangle(ctx,0,0,self.width, ls.border_width)
                ctx.restore()
                ctx.translate(0, ls.border_width)

        ctx.restore()

    def draw_user_status (self, ctx, status):
        """ draw user status"""

        if not self.window.window:
            return

        if not self.layout.status:
            return

        ls = self.layout.status

        if status.buffer == None:
            buffer = gtk.gdk.Pixmap(self.window.window, 
                                    int(self.width), int(self.status_height), -1)

            text_ctx = buffer.cairo_create()
            self.clear_cairo_context(text_ctx)
            text_ctx.set_source_rgba(1,1,1,1)

            status_format = "<span foreground=\"%s\"><b>%s</b></span> <span foreground=\"%s\">%s</span>"
            status_text =  status_format % (ls.name_color.get_str(), 
                                            status.GetUser().GetScreenName(),
                                            ls.text_color.get_str(),
                                            escape_for_pango_markup(status.GetText()))


            self.draw_text(text_ctx, status_text, 
                           self.user_icon_size + 4, 2, 
                           self.item_font, 9, 
                           self.width-5-self.user_icon_size, 
                           allignment=pango.ALIGN_LEFT, 
                           weight=5,
                           ellipsize=pango.ELLIPSIZE_NONE)
            status.buffer = buffer

        ctx.save()
        #ctx.set_source_rgba(0,0,0,0)
        #ctx.rectangle(0,0,self.width, self.status_height)
        #ctx.clip()
        ctx.set_operator(cairo.OPERATOR_OVER)
        ctx.set_source_pixmap(status.buffer, 0, 0)
        ctx.paint()
        ctx.restore()

        self.draw_user_icon(ctx, 2, 2, self.user_icon_size, self.user_icon_size, 4.0, 1, self.users[status.GetUser().GetName()])

    # from PidginScreenlet
    # Draw User Icon (Display Picture) 
    def draw_user_icon (self, ctx, x, y, w, h, rounding_radius, transparency, user):
        if user.iconPixbuf:
            ctx.save()
            ctx.translate(x, y)
            r = rounding_radius
            if r: self.round_rectangle(ctx,0,0,w,h,r,r,r,r)
            else: ctx.rectangle(0,0,w,h)
            ctx.clip()
            ctx.set_source_pixbuf(user.iconPixbuf, 0, 0)
            ctx.paint_with_alpha(transparency)
            #ctx.paint()
            ctx.restore()

    # from PidginScreenlet
    # r1: top-left radius, r2: top-right radius, 
    # r3: bottom-left radius, r4: bottom-right radius
    def round_rectangle(self, ctx, x, y, width, height, r1, r2, r3, r4):
        ctx.move_to(x+r1, y)
        ctx.line_to(x+width-r2,y)
        # top right
        if r2 > 0:
                ctx.arc(x+width-r2, y+r2, r2, -PI/2.0, 0)
        ctx.line_to(x+width,y+height-r4)
        # bottom right
        if r4 > 0:
                ctx.arc(x+width-r4, y+height-r4, r4, 0, PI/2.0)
        ctx.line_to(x+r3, y+height)
        # bottom left
        if r3 > 0:
                ctx.arc(x+r3, y+height-r3, r3, PI/2.0, PI)
        ctx.line_to(x,y+r1)
        # top left
        if r1 > 0:
                ctx.arc(x+r1, y+r1, r1, PI, PI*1.5)
        
if __name__ == "__main__":
    # create new session
    import screenlets.session
    screenlets.session.create_session(TwitterScreenlet)
